# koroFileHeader at Yungoal acer
# Create: 2021-06-18 13:12:21
# LastEdit: 2021-07-02 11:04:18
"""丝芙兰密码策略

Linux:
1. 配置禁止使用最近用过的5个密码
2. 配置最小密码长度为8
3. 设置密码复杂度
4. 设置密码有效期90天
5. 配置用户输入错误密码5次锁住，10分钟后解锁。

Windows:
1. 设置密码复杂度
2. 设置密码长度
3. 设置密码有效期
"""
__author__ = '749B'

import os, sys
BASE_DIR = os.getcwd()
sys.path.append(BASE_DIR)

import textwrap
# pylint: disable = import-error
from azure_client.models import RuncmdCheck, RuncmdTask
from utils.blueking import job_start, job_success, job_fail

from typing import Optional, List, Tuple
_Check = Tuple[bool, str]


class PasswordCheck(RuncmdCheck):

    def parse_linux(self, task: RuncmdTask) -> _Check:
        errors = []
        complexity, minlen, pass_max_days, remember, lock = self.std_out.strip().splitlines()
        # complexity 复杂度， lcredit = -1|ucredit = -1|dcredit = -1|ocredit = -1
        if complexity == 'NotSet':
            errors.append("复杂度未设置")
        else:
            complexity_list = [i.replace(' ', '') for i in complexity.split('|')]
            if not('lcredit=-1' in complexity_list and 'ucredit=-1' in complexity_list and 'dcredit=-1' in complexity_list and 'ocredit=-1' in complexity_list):
                errors.append("复杂度不合规")
        # minlen 最小密码长度为 8, minlen = 8
        if minlen == 'NotSet':
            errors.append("密码长度未设置")
        else:
            minlen_int = None
            minlen_list = minlen.replace(' ', '').split('=')
            if len(minlen_list) == 2:
                if minlen_list[1].isdigit():
                    minlen_int = int(minlen_list[1])
            if minlen_int is None:
                errors.append("密码长度设置非法")
            elif minlen_int < 8:
                errors.append("密码长度太短%s" % minlen_int)
        # pass_max_days 密码有效期90明天, PASS_MAX_DAYS 90
        if pass_max_days == 'NotSet':
            errors.append("密码有效期未设置")
        else:
            pass_max_days_int = None
            pass_max_days_list = pass_max_days.strip().split()
            if len(pass_max_days_list) >= 2:
                if pass_max_days_list[1].isdigit():
                    pass_max_days_int = int(pass_max_days_list[1])
                if pass_max_days_int is None:
                    errors.append("密码有效期设置非法")
                elif pass_max_days_int != 90:
                    errors.append("密码有效期设置不正确%s" % pass_max_days_int)
        # remember 记住最近 5 个密码, remember=5
        if remember == 'NotSet':
            errors.append("禁用最近的5个密码未设置")
        else:
            remember_int = None
            remember_list = remember.replace(' ', '').split('=')
            if len(remember_list) == 2:
                if remember_list[1].isdigit():
                    remember_int = int(remember_list[1])
            if remember_int is None:
                errors.append("禁用最近的5个密码设置非法")
            elif remember_int != 5:
                errors.append("禁用最近的5个密码设置不正确%s" % remember_int)
        # lock 用户输入错误密码5次锁住，10分钟（600秒）后解锁， deny=5|unlock_time=600
        if lock == 'NotSet':
            errors.append("用户锁定未设置")
        else:
            lock_list = [i.replace(' ', '') for i in lock.split('|')]
            if not('deny=5' in lock_list and 'unlock_time=600' in lock_list):
                errors.append("用户锁定不合规")
        if errors:
            return False, '|'.join(errors)
        return True, "OK"

    def parse_windows(self, task: RuncmdTask) -> _Check:
        errors = []
        complexity, length, age = self.std_out.strip().splitlines()
        # complexity 密码复杂度，需要为 1
        if complexity != '1':
            errors.append("复杂度未配置")
        # length 密码长度，需要 8
        if length.isdigit():
            length_int = int(length)
            if length_int < 8:
                errors.append("密码长度不够%s" % length_int)
        else:
            errors.append("密码长度未设置")
        # age 密码有效期 90
        if age.isdigit():
            age_int = int(age)
            if age_int != 90:
                errors.append("密码有效期不合规%s" % age_int)
        else:
            errors.append("密码有效期未设置")
        if errors:
            return False, '|'.join(errors)
        return True, "OK"


def main(args: Optional[List[str]] = None) -> None:
    """主函数"""
    check_obj = PasswordCheck(args, SCRIPTS['linux']['cmd'], SCRIPTS['windows']['cmd'], SCRIPTS['linux']['check_items'])
    check_obj.run()

    if check_obj.fail_count:
        job_fail("不合规的记录数: %s" % check_obj.fail_count)
    job_success()


# pylint: disable = anomalous-backslash-in-string
SCRIPTS = {
    'windows': {
        'check_items': ["PasswordComplexity", "MinimumPasswordLength", "MaximumPasswordAge"],
        'cmd': textwrap.dedent("""\
            $Script:exported = $false
            $cfgfile = "${env:TEMP}/password_policy_check.cfg"
            function check($item) {
                if (!$Script:exported) {
                    secedit /export /cfg "$cfgfile" /quiet
                    if (!$?) {
                        Write-Error "需要administrator权限才能导出安全策略"
                        exit 1
                    }
                    $Script:exported = $true
                }
                $config = Get-Content -path "$cfgfile"
                for ($i=0; $i -lt $config.Length; $i++) {
                    $config_line = $config[$i] -split "="
                    if ($config_line[0].Trim() -eq "$item") {
                        Write-Host $config_line[1].Trim()
                        return
                    }
                }
                Write-Host NotExist
            }
            check("PasswordComplexity")
            check("MinimumPasswordLength")
            check("MaximumPasswordAge")
            """)},
    'linux': {
        'check_items': ['Complexity', 'minlen', 'PASS_MAX_DAYS', 'remember', 'lock'],
        # 字符串中的\n会被Python转义成换行符，这里需要把\n作为2个字符在Python中传递下去
        'cmd': textwrap.dedent("""\
            res=$(grep -E "^(\s*)([duol]credit|minclass)\s*=\s*\S+(\s*#.*)?\s*$" /etc/security/pwquality.conf | awk -v ORS='|' '{print $0}' | sed 's/.$/\\n/') 
            [ "$res" != "" ] && echo "$res" || echo "NotSet"
            grep -E "^(\s*)minlen\s*=\s*\S+(\s*#.*)?\s*$" /etc/security/pwquality.conf || echo "NotSet"
            # Centos6.3 系统上 awk 3.1.7 正则匹配遇到问题
            res=$(grep -E "^(\s*)PASS_MAX_DAYS\s+\S+(\s*#.*)?\s*$" /etc/login.defs | tr '\t' ' ')
            [ "$res" != "" ] && echo "$res" || echo "NotSet"
            res=$(grep -E '^\s*password\s+sufficient\s+pam_unix.so(\s+\S+)*\s+remember=[0-9]+(\s+.*)?$' /etc/pam.d/system-auth | awk '{for(i=1;i<=NF;i++)print($i)}' | grep '^remember=') 
            [ "$res" != "" ] && echo "$res" || echo "NotSet"
            res=$(grep -E "^\s*auth\s+required\s+pam_tally2.so(\s+.*)$" /etc/pam.d/postlogin | awk '{for(i=4;i<=NF;i++)printf("%s%s", $i, i==NF?"\\n":"|")}')
            [ "$res" != "" ] && echo "$res" || echo "NotSet"
        """)},
    }

if __name__ == '__main__':

    job_start()

    ###### 可在此处开始编写您的脚本逻辑代码
    ###### iJobs中执行脚本成功和失败的标准只取决于脚本最后一条执行语句的返回值
    ###### 如果返回值为0，则认为此脚本执行成功，如果非0，则认为脚本执行失败

    main()
